package com.cocotingo.snail.template;

import java.awt.Color;
import java.awt.GraphicsConfiguration;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Hashtable;

import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.cocotingo.snail.AcceleratedImage.ImageSourceLoader;
import com.cocotingo.snail.template.resource.AcceleratedImageResource;
import com.cocotingo.snail.template.resource.Resource;
import com.cocotingo.snail.template.resource.StretchableImageResource;

public class TemplateLoader {
	
	private final Hashtable<String, Template> templates;
	private final Hashtable<String, Resource> resources;
	private final Hashtable<String, Color> colorCache;
	private final GraphicsConfiguration configuration;
	private DocumentBuilderFactory builderFactory;
	public static final String VERSION = "1.0";
	
	public TemplateLoader(GraphicsConfiguration gc) {
		templates = new Hashtable<String, Template>();
		resources = new Hashtable<String, Resource>();
		colorCache = new Hashtable<String, Color>();
		this.configuration = gc;
		colorCache.put("null", new Color(0x00ffffff, true));
		builderFactory = DocumentBuilderFactory.newInstance();
		builderFactory.setIgnoringComments(true);
		builderFactory.setIgnoringElementContentWhitespace(true);
	}
	
	public SealedView createView(String templateID) throws TemplateException {
		return getTemplate(templateID).createView();
	}
	
	public Template getTemplate(String id) {
		return templates.get(id);
	}
	
	public GraphicsConfiguration getGraphicsConfiguration() {
		return configuration;
	}
	
	public Template[] getAllTemplates() {
		return templates.values().toArray(new Template[0]);
	}
	
	public Resource getResource(String id) {
		return resources.get(id);
	}
	
	public Resource[] getAllResources() {
		return resources.values().toArray(new Resource[0]);
	}
	
	public void load(String xmlFileName, ResourceLoader resLoader) throws TemplateException {
		DocumentBuilder builder;
		try {
			builder = builderFactory.newDocumentBuilder();
			Document doc = builder.parse(resLoader.loadResource(xmlFileName));
			visitDocument(doc, resLoader);
		} catch (ParserConfigurationException e) {
			throw new TemplateException(e);
		} catch (SAXException e) {
			throw new TemplateException(e);
		} catch (IOException e) {
			throw new TemplateException(e);
		}
	}
	
	public Color getColor(String hex) throws TemplateException {
		if (colorCache.containsKey(hex)) return colorCache.get(hex);
		else {
			int value = hexToInteger(hex);
			Color color = new Color(value);
			colorCache.put(hex, color);
			return color;
		}
	}

	private static int hexToInteger(String hex) throws TemplateException {
		if (hex.length() > 2 && hex.startsWith("0x")) {
			try {
				int rst = 0;
				int j = 0;
				for (int i = hex.length() - 1; i >= 2; i--, j += 4) {
					rst += hexCharacterToInteger(hex.charAt(i)) << j;
				}
				return rst;
			} catch (TemplateException e) {
				throw new TemplateException("can not recognize hex integer: " + hex, e);
			}
		} else {
			throw new TemplateException("can not recognize hex integer: " + hex);
		}
	}
	
	private static int hexCharacterToInteger(char ch) throws TemplateException {
		if (ch >= '0' && ch <= '9') return ch - '0';
		else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
		else if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
		else throw new TemplateException("can not recognize hex character: " + ch);
	}
	
	private void visitDocument(Document doc, ResourceLoader resLoader) throws TemplateException {
		NodeList nodes = doc.getChildNodes();
		// find root "ViewTemplateDefinitions" node
		for (int i = 0; i < nodes.getLength(); i++) {
			Node node = nodes.item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE &&
				node.getNodeName().equals("TemplateDefinitions")) {
				visitRootNode(node, resLoader);
				return;
			}
		}
		throw new TemplateException("missing template definitions node");
	}
	
	private void visitRootNode(Node root, ResourceLoader resLoader) throws TemplateException {
		NamedNodeMap attrs = root.getAttributes();
		Node verNode = attrs.getNamedItem("version");
		if (verNode != null) {
			if (!verNode.getNodeValue().equals(VERSION)) throw new TemplateException("template definitions version dismatch: " + verNode.getNodeValue());
		} else {
			throw new TemplateException("missing version attribute in template definitions node");
		}
		
		NodeList nodes = root.getChildNodes();
		for (int i = 0; i < nodes.getLength(); i++) {
			Node node = nodes.item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				if (node.getNodeName().equals("Template")) {
					visitTemplateNode(node);
				} else if (node.getNodeName().equals("Resources")) {
					visitResourcesNode(node, resLoader);
				} else {
					throw new TemplateException("unknown element in template definitions: " + node.getNodeName());
				}
			}
		}
	}
	
	private void visitTemplateNode(final Node node) throws TemplateException {
		NamedNodeMap map = node.getAttributes();
		Node idNode = map.getNamedItem("id");
		if (idNode == null) throw new TemplateException("missing id attribute in template node");
		final String id = idNode.getNodeValue();
		// validate template id
		if (templates.containsKey(id)) throw new TemplateException("duplicate template id: " + id);
		final Template template = new Template(this, id, node);
		templates.put(id, template);
	}
	
	private void visitResourcesNode(Node node, final ResourceLoader resLoader) throws TemplateException {
		NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				NamedNodeMap attrs = child.getAttributes();
				String id = getIDAttributeFromResourceNode(attrs.getNamedItem("id"));
				final String source = getSourceAttributeFromResourceNode(attrs.getNamedItem("source"));
				if (child.getNodeName().equals("AcceleratedImage")) {
					ImageSourceLoader loader = new ImageSourceLoader() {
						@Override
						public void unloadImage(BufferedImage image) {
							image.flush();
						}
						@Override
						public BufferedImage loadImage() {
							try {
								return ImageIO.read(resLoader.loadResource(source));
							} catch (IOException e) {
							}
							return null;
						}
					};
					AcceleratedImageResource res = new AcceleratedImageResource(id, configuration, loader);
					resources.put(id, res);
				} else if (child.getNodeName().equals("StretchableImage")) {
					int[] margins = getMarginsAttributeFromResourceNode(attrs.getNamedItem("margins"));
					ImageSourceLoader loader = new ImageSourceLoader() {
						@Override
						public void unloadImage(BufferedImage image) {
							image.flush();
						}
						@Override
						public BufferedImage loadImage() {
							try {
								return ImageIO.read(resLoader.loadResource(source));
							} catch (IOException e) {
							}
							return null;
						}
					};
					StretchableImageResource res = new StretchableImageResource(id, configuration, loader, margins);
					resources.put(id, res);
				} else {
					throw new TemplateException("unknown resource type: " + child.getNodeName());
				}
			}
		}
	}
	
	private String getIDAttributeFromResourceNode(Node node) throws TemplateException {
		if (node == null) throw new TemplateException("missing id attribute in resource node");
		String id = node.getNodeValue();
		if (resources.containsKey(id)) throw new TemplateException("duplicate resource id: " + id);
		return id;
	}
	
	private String getSourceAttributeFromResourceNode(Node node) throws TemplateException {
		if (node == null) throw new TemplateException("missing source attribute in resource node");
		return node.getNodeValue();
	}
	
	private int[] getMarginsAttributeFromResourceNode(Node node) throws TemplateException {
		if (node == null) throw new TemplateException("missing margins attribute in resource node");
		String[] strs = node.getNodeValue().split(",");
		if (strs.length < 4) throw new TemplateException("not enough args in margins attribute");
		else if (strs.length > 4) throw new TemplateException("too many args in margins attribute");
		int[] margins = new int[4];
		for (int i = 0; i < 4; i++) {
			try {
				margins[i] = Integer.valueOf(strs[i].trim());
			} catch (NumberFormatException e) {
				throw new TemplateException("invalid arg in margins attribute", e);
			}
		}
		return margins;
	}

}
